跳到主要内容

JWT 与 Session 的常见问题

什么是会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于 session 方式、基于 token 方式等。

基于 session 的认证方式由 Servlet 规范定制,服务端要存储 session 信息需要占用内存资源,客户端需要支持 cookie 基于 token 的方式则一般不需要服务端存储 token,并且不限制客户端的存储方式。

基于 session 的认证方式

基于 session 的认证方式:它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在 session(当前会话)中,发给客户端的 session_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或 session 过期销毁时,客户端的 session_id 也就无效了。

在分布式的环境下,基于 session 的认证会出现一个问题,每个应用服务都需要在 session 中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将 session 信息带过去,否则会重新认证。

这个时候,通常的做法有下面几种:

Session 复制:多台应用服务器之间同步 Session,使 Session 保持一致,对外透明。 Session 黏贴:当用户访问集群中某台服务器后,强制指定后续所有请求均落到此机器上。 Session 集中存储:将 Session 存入分布式缓存中,所有服务器应用实例统一从分布式缓存中存取 Session。

总体来讲,基于 session 认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session 机制方式基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高 session 的复制、黏贴及存储的容错性。

基于 token 的认证方式

基于 token 的认证方式:它的交互流程是,用户认证成功后,服务端生成一个 token 发给客户端,客户端可以放到 cookie 或 localStorage 等存储中,每次请求时带上 token,服务端收到 token 通过验证后即可确认用户身份。

基于 token 的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把 token 存在任意地方,并且可以实现 web 和 app 统一认证机制。其缺点也很明显,token 由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token 的签名验签操作也会给 cpu 带来额外的处理负担。

什么是认证(Authentication)

通俗地讲就是验证当前用户的身份,证明 “你是你自己”(比如:你每天上下班打卡,都需要通过指纹打卡,当你的指纹和系统里录入的指纹相匹配时,就打卡成功)

互联网中的认证:

  • 用户名密码登录
  • 邮箱发送登录链接
  • 手机号接收验证码
  • 只要你能收到邮箱/验证码,就默认你是账号的主人

什么是授权(Authorization)

用户授予第三方应用访问该用户某些资源的权限

你在安装手机应用的时候,APP 会询问是否允许授予权限(访问相册、地理位置等权限) 你在访问微信小程序时,当登录时,小程序会询问是否允许授予权限(获取昵称、头像、地区、性别等个人信息)

实现授权的方式有:cookie、session、token、OAuth

什么是凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份

在互联网应用中,一般网站(如掘金)会有两种模式,游客模式和登录模式。游客模式下,可以正常浏览网站上面的文章,一旦想要点赞/收藏/分享文章,就需要登录或者注册账号。当用户登录成功后,服务器会给该用户使用的浏览器颁发一个令牌(token),这个令牌用来表明你的身份,每次浏览器发送请求时会带上这个令牌,就可以使用游客模式下无法使用的功能。

透明令牌是什么?

在 OAuth2 体系中认证通过后返回的令牌信息分为两大类:不透明令牌(opaque tokens) 和 透明令牌(not opaque tokens)。

不透明令牌 就是一种无可读性的令牌,一般来说就是一段普通的 UUID 字符串。使用不透明令牌会降低系统性能和可用性,并且增加延迟,因为资源服务不知道这个令牌是什么,代表谁,需要调用认证服务器获取用户信息接口,如下就是我们在资源服务器中的配置,需要指明认证服务器的接口地址。

security:
oauth2:
resource:
user-info-uri: http://localhost:5000/user/current/get
id: account-service

透明令牌的典型代表就是 JWT 了,用户信息保存在 JWT 字符串中,资源服务器自己可以解析令牌不再需要去认证服务器校验令牌。

使用不透明令牌的 access_token,在微服务体系中这种中心化的授权服务可能会成为瓶颈,所以可用使用 JWT 来替换不透明的 access_token,也叫去中心化。

即 JWT 将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息

JWT 的不足

1、无状态和吊销无法两全,如果某个用户令牌异常(比如有黑客在干坏事),我们想要吊销这个用户的令牌,但是却没有办法在 AuthService 上进行统一吊销,一般需要等到这个JWT令牌自然过期才能吊销。又假设我们在 AuthService 上对某个用户的信息进行了更新,那么相关的 Claims 信息也必须要等到这个老的 JWT 过期后重新登录或刷新后产生了新的 JWT 后才能更新。

2、JWT 的大小会随着 Claims 的数量增多,也会导致 JWT 的大小会变大,从而也会导致传输的开销增大。

更多缺点参考自 (译)别再使用 JWT 作为 Session 系统!问题重重且很危险。

Token 和 Session 的区别?

Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。

Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。

所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。

而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。

所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。

Token 的存储位置

token 由前端存在 localStorage 或者 cookie 是不安全的。更安全的做法是将 accessToken 存在内存中,refreshToken 由 backend 通过 set-cookie 的方式存在 http-only 的 cookie 中。

使用 JWT 如何保存上下文数据?

看了一圈,没有说这点怎么处理,学到再更新...

下面有个简单的实现方式:

JWT 如何保存上下文数据简单实现

参考 【项目实践】一文带你搞定Session和JWT

JWT 不像 Session 把用户信息直接存储起来,所以 JWT 的上下文对象要靠我们自己来实现。首先我们定义一个上下文类,这个类专门存储 JWT 解析出来的用户信息。我们要用到 ThreadLocal,以防止线程冲突:

public final class UserContext {
private static final ThreadLocal<String> user = new ThreadLocal<String>();

public static void add(String userName) {
user.set(userName);
}

public static void remove() {
user.remove();
}

/**
* @return 当前登录用户的用户名
*/
public static String getCurrentUserName() {
return user.get();
}
}

这个类创建好之后我们还需要在拦截器里做下处理:

public class LoginInterceptor extends HandlerInterceptorAdapter {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
...省略之前写的代码

// 从请求头中获取token字符串并解析
Claims claims = JwtUtil.parse(request.getHeader("Authorization"));
// 已登录就直接放行
if (claims != null) {
// 将我们之前放到token中的userName给存到上下文对象中
UserContext.add(claims.getSubject());
return true;
}

...省略之前写的代码
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 请求结束后要从上下文对象删除数据,如果不删除则可能会导致内存泄露
UserContext.remove();
super.afterCompletion(request, response, handler, ex);
}
}

这样一个上下文对象就做好了,用法和之前一样,可以在程序的其他地方直接获取到数据,我们在 Service 层中来使用它:

public void doSomething() {
String currentUserName = UserContext.getCurrentUserName();
System.out.println("Service层---当前用户登录名:" + currentUserName);
}

具体看原文,这里只是提供一个思路

Reference

(译)别再使用 JWT 作为 Session 系统!问题重重且很危险。 【项目实践】一文带你搞定Session和JWT SpringCloud Alibaba微服务实战十七 - JWT认证